/**
 * Copyright © 2004-2018 ***.All Rights Reserved.
 */
package com.framework.common.jdbc.interceptor;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.jdbc.ConnectionLogger;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.MappedStatement.Builder;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 
 * @version V1.0
 * @author zhoubb
 * @Date 2018-12-04
 * @since JDK 1.7
 */
public abstract class PagingInterceptor implements Interceptor {


	private static final String ORDER_BY_REGEX = "order\\s+by";
	private static final String UNION = "union";
	private static final String FROM = "from";

	public Object intercept(Invocation invocation) throws Throwable {
		MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
		Object parameter = invocation.getArgs()[1];
		BoundSql boundSql = mappedStatement.getBoundSql(parameter);
		String originalSql = boundSql.getSql().trim();
		Object parameterObject = boundSql.getParameterObject();
		PageParaBean page = findPageBean(parameterObject);
		if (page != null) {
			if (page.isCountCompute()) {
				PreparedStatement countStmt = null;
				ResultSet rs = null;
				Connection connection = null;
				try {
					String countSql = getCountSql(originalSql);
					connection = getConnection((Executor) invocation.getTarget(), mappedStatement);
					countStmt = connection.prepareStatement(countSql);
					BoundSql countBS = buildNewBoundSql(mappedStatement, boundSql, countSql);
					DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement,
							parameterObject, countBS);

					parameterHandler.setParameters(countStmt);
					rs = countStmt.executeQuery();
					if (rs.next()) {
						page.setTotal(Integer.valueOf(rs.getInt(1)));
					}
				} finally {
					if (rs != null) {
						rs.close();
					}
					if (countStmt != null) {
						countStmt.close();
					}
				}
			}
			if (page.getRows().intValue() != -1) {
				String pagingsql = getPagingSQL(page, originalSql);
				final BoundSql newBoundSql = buildNewBoundSql(mappedStatement, boundSql, pagingsql);
				MappedStatement newMs = buildNewMappedStatement(mappedStatement, new SqlSource() {
					public BoundSql getBoundSql(Object parameterObject) {
						return newBoundSql;
					}
				});
				invocation.getArgs()[0] = newMs;
				invocation.getArgs()[5] = newBoundSql;
			}
		}
		Object result = invocation.proceed();
		return result;
	}

	private Connection getConnection(Executor executor, MappedStatement mappedStatement) throws SQLException {
		Connection connection = executor.getTransaction().getConnection();
		Log statementLog = mappedStatement.getStatementLog();
		if (statementLog.isDebugEnabled()) {
			return ConnectionLogger.newInstance(connection, statementLog, 0);
		}
		return connection;
	}

	private PageParaBean findPageBean(Object parameterObject) {
		if ((parameterObject instanceof Map)) {
			for (Object value : ((Map) parameterObject).values()) {
				if ((value instanceof PageParaBean)) {
					return (PageParaBean) value;
				}
			}
		} else if ((parameterObject instanceof PageParaBean)) {
			return (PageParaBean) parameterObject;
		}
		return null;
	}

	private MappedStatement buildNewMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
		Builder builder = new Builder(ms.getConfiguration(), ms.getId(), newSqlSource,
				ms.getSqlCommandType());
		builder.resource(ms.getResource());
		builder.fetchSize(ms.getFetchSize());
		builder.statementType(ms.getStatementType());
		builder.keyGenerator(ms.getKeyGenerator());
		builder.keyProperty(StringUtils.join(ms.getKeyProperties(), ","));
		builder.keyColumn(StringUtils.join(ms.getKeyColumns(), ","));
		builder.timeout(ms.getTimeout());
		builder.lang(ms.getLang());
		builder.resulSets(StringUtils.join(ms.getResulSets(), ","));
		builder.resultOrdered(ms.isResultOrdered());
		builder.parameterMap(ms.getParameterMap());
		builder.resultMaps(ms.getResultMaps());
		builder.resultSetType(ms.getResultSetType());
		builder.cache(ms.getCache());
		builder.flushCacheRequired(ms.isFlushCacheRequired());
		builder.useCache(ms.isUseCache());
		builder.databaseId(ms.getDatabaseId());
		return builder.build();
	}

	private BoundSql buildNewBoundSql(MappedStatement ms, BoundSql boundSql, String sql) {
		BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), sql, boundSql.getParameterMappings(),
				boundSql.getParameterObject());
		for (ParameterMapping mapping : boundSql.getParameterMappings()) {
			String prop = mapping.getProperty();
			if (boundSql.hasAdditionalParameter(prop)) {
				newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
			}
		}
		return newBoundSql;
	}

	protected abstract String getPagingSQL(PageParaBean paramPageParaBean, String paramString);

	protected String getCountSql(String targetSql) {
		String sql = targetSql;
		StringBuilder sqlBuilder = new StringBuilder(sql);
		int orderByPos = 0;
		if (((orderByPos = achieveRegexLastPos(sqlBuilder, "order\\s+by")) != -1)
				&& (isLastOrderByClause(sqlBuilder.substring(orderByPos)))) {
			sqlBuilder.delete(orderByPos, sqlBuilder.length());
		}
		if (achieveWordFirstPos(sqlBuilder, "union") != -1) {
			sqlBuilder.insert(0, "select count(1) as total_count from ( ").append(" ) count_alias");
			return sqlBuilder.toString();
		}
		sqlBuilder = trimToCountSQL(sqlBuilder);
		int fromPos = achieveWordFirstPos(sqlBuilder, "from");
		if (fromPos != -1) {
			sqlBuilder.delete(0, fromPos);
			sqlBuilder.insert(0, "select count(1) as total_count ");
		}
		return sqlBuilder.toString();
	}

	private boolean isLastOrderByClause(String orderByClause) {
		return Pattern.compile(".*\\b(or|on|where|select|and|join|group|having)\\b.*", 2).matcher(orderByClause)
				.matches();
	}

	private StringBuilder trimToCountSQL(StringBuilder sqlBuilder) {
		int fromPos = achieveWordFirstPos(sqlBuilder, "from");
		if (fromPos != -1) {
			String prefix = sqlBuilder.substring(0, fromPos + "from".length());
			Matcher matcher = Pattern.compile("(\\(\\s*select.*from)", 2).matcher(prefix);
			if (matcher.find()) {
				int start = matcher.start();
				int end = matcher.end();
				sqlBuilder.replace(start, end, "");
				return trimToCountSQL(sqlBuilder);
			}
		}
		return sqlBuilder;
	}

	private int achieveWordFirstPos(StringBuilder sb, String word) {
		Matcher matcher = Pattern.compile("\\b" + word + "\\b", 2).matcher(sb);
		if (matcher.find()) {
			return matcher.start();
		}
		return -1;
	}

	private int achieveRegexLastPos(StringBuilder sb, String regex) {
		Matcher matcher = Pattern.compile(regex, 2).matcher(sb);
		int pos = -1;
		while (matcher.find()) {
			pos = matcher.start();
		}
		return pos;
	}

	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	public void setProperties(Properties properties) {
	}
}
